package com.tian.service.impl.pay;

import com.alibaba.fastjson.JSON;
import com.tian.common.enums.ChargingStatusEnum;
import com.tian.common.enums.ExchangeCodeStatusEnum;
import com.tian.common.enums.PayStatusEnum;
import com.tian.common.exception.BusinessException;
import com.tian.common.strategy.PaymentStrategy;
import com.tian.common.util.*;
import com.tian.dto.user.ChargeUserLoginResDto;
import com.tian.dto.market.exchange.CheckExchangeCodeRespDto;
import com.tian.dto.market.exchange.ExchangeCodeDto;
import com.tian.dto.pay.ChargePayReqDto;
import com.tian.entity.ChargePayOrder;
import com.tian.mapper.ChargePayOrderMapper;
import com.tian.service.pay.PayBaseService;
import com.tian.service.pay.PaymentService;
import com.tian.service.pay.PrePayService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * {@code @description:} 发起支付订单的业务实现类
 *
 * @author tianwc 公众号：Java后端技术全栈
 * 在线刷题 1200+java面试题和1000+篇技术文章：<a href="https://woaijava.cc/">博客地址</a>
 * {@code @date:} 2025-01-20 9:47
 * {@code @version:} 1.0
 */
@Slf4j
@Service
public class PrePayServiceImpl implements PrePayService {
    private static final String CHARGE_PAY_ORDER_NO_PREFIX = "CHARGE_PAY_ORDER_NO";
    @Resource
    protected RedissonClient redissonClient;
    @Resource
    private ChargePayOrderMapper chargePayOrderMapper;
    @Value("${pay.isTest}")
    private Boolean isTest;
    @Resource
    private PayBaseService payBaseService;

    @Override
    public CommonResult<String> doPay(ChargePayReqDto chargePayReqDto) {
        this.checkParams(chargePayReqDto);
        this.checkResubmit(chargePayReqDto.getAmount(), UserCacheUtil.getUser().getId());
        //获取当前登录信息
        ChargeUserLoginResDto user = UserCacheUtil.getUser();
        //如果是支付充电记录，则校验支付订单状态
        this.checkPayChargingRecordStatus(chargePayReqDto);
        //生成支付订单
        ChargePayOrder chargePayOrder = this.getChargePayOrder(chargePayReqDto, user);
        //业务处理
        this.doBusiness(chargePayOrder, chargePayReqDto);
        //调用支付接口
        String prepayResult = this.doPay(chargePayReqDto, user, chargePayOrder);
        //异步业务处理
        this.asyncHandlePay(chargePayReqDto, chargePayReqDto.getUserCouponIdList(), chargePayOrder);
        //缓存优惠券使用中状态
        this.cacheCouponUsing(chargePayReqDto.getUserCouponIdList(), user);
        //返回
        return CommonResult.success(prepayResult);
    }

    private void doBusiness(ChargePayOrder chargePayOrder, ChargePayReqDto chargePayReqDto) {
        BigDecimal amount = chargePayReqDto.getAmount();
        Map<Long, BigDecimal> couponAmountMap = new HashMap<>();
        // 计算优惠券金额
        List<Long> userCouponIdList = chargePayReqDto.getUserCouponIdList();
        BigDecimal couponAmount = BigDecimal.ZERO;
        if (!CollectionUtils.isEmpty(userCouponIdList)) {
            //优惠券校验
            String result = payBaseService.checkCoupons(chargePayReqDto);
            if (!StringUtil.isBlank(result)) {
                throw new BusinessException(result);
            }
            amount = amount.subtract(payBaseService.calculateCouponAmount(couponAmountMap, amount, userCouponIdList));
            //couponAmountMap遍历统计优惠总金额
            for (Map.Entry<Long, BigDecimal> entry : couponAmountMap.entrySet()) {
                couponAmount = couponAmount.add(entry.getValue());
            }
        }
        String exchangeCode = chargePayReqDto.getExchangeCode();
        RLock usingExchangeCodeLock = null;
        try {
            //关于兑换码
            //第一步：校验是否参数中有兑换码
            if (!StringUtil.isBlank(exchangeCode)) {
                //第二步：校验兑换码是否真实存在
                RBucket<String> bucket = redissonClient.getBucket(RedisConstantPre.EXCHANGE_CODE_KEY + chargePayReqDto.getExchangeCode());
                ExchangeCodeDto exchangeCodeDto = JSON.parseObject(bucket.get(), ExchangeCodeDto.class);
                if (exchangeCodeDto == null) {
                    log.error("兑换码不存在");
                    throw new BusinessException("兑换码不存在");
                }

                //第三步：给兑换码上 分布式锁，防止重复使用
                usingExchangeCodeLock = redissonClient.getLock(RedisConstantPre.EXCHANGE_CODE_USING_LOCK_KEY + exchangeCodeDto.getId());
                usingExchangeCodeLock.lock();
                //第四步：校验兑换码是否可用（调用market-service的接口获取）
                CheckExchangeCodeRespDto checkExchangeCodeRespDto = payBaseService.checkExchangeCode(chargePayReqDto);
                couponAmount = checkExchangeCodeRespDto.getCouponAmount();
                amount = amount.subtract(couponAmount);
            }
            chargePayOrder.setAmount(amount);
            //优惠金额
            chargePayOrder.setDiscountAmount(couponAmount);
            //生成支付订单
            chargePayOrderMapper.insert(chargePayOrder);
            if (!StringUtil.isBlank(exchangeCode)) {
                //第五步：在生成支付单后，使用（采用redis缓存 set key=固定前缀+兑换码id，value=兑换码状态，过期时间），如果能获取到，证明兑换码已被使用，则不允许重复使用
                //主要是防止消费者在使用兑换码时，时间差离过大，导致兑换码状态未及时更新，导致重复使用
                RBucket<Object> cacheExchangeCodeUsing = redissonClient.getBucket(RedisConstantPre.EXCHANGE_CODE_USING_KEY + chargePayOrder.getId());
                if (cacheExchangeCodeUsing.isExists()) {
                    log.error("兑换码已被使用");
                    throw new BusinessException("兑换码已被使用");
                }
                cacheExchangeCodeUsing.set(exchangeCode, 60, TimeUnit.SECONDS);

                //第六步：采用MQ模式，异步修改兑换码状态为使用中
                payBaseService.sendMsgUpdateExchangeCodeStatus(chargePayReqDto.getCode(), chargePayOrder.getUserId()
                        , chargePayOrder.getId(), ExchangeCodeStatusEnum.USING.getCode());
            }
        } finally {
            if (usingExchangeCodeLock != null) {
                usingExchangeCodeLock.unlock();
            }
        }
        //插入订单优惠券关联表
        if (!CollectionUtils.isEmpty(userCouponIdList)) {
            payBaseService.insertOrderCoupon(userCouponIdList, chargePayOrder, couponAmountMap);
        }
    }

    private String doPay(ChargePayReqDto chargePayReqDto, ChargeUserLoginResDto user, ChargePayOrder chargePayOrder) {
        //组装支付参数
        PaymentService paymentService = PaymentStrategy.getPaymentService(chargePayReqDto.getChannelId());
        if (paymentService == null) {
            throw new BusinessException("暂不支持该支付方式");
        }
        String msg = user.getNickName() + "充值";
        String prepay = paymentService.prepay(chargePayOrder, msg, isTest);
        //返回失败
        if (prepay == null || StringUtil.isBlank(prepay)) {
            throw new BusinessException("支付失败");
        }
        return prepay;
    }

    private void asyncHandlePay(ChargePayReqDto chargePayReqDto, List<Long> userCouponIdList, ChargePayOrder chargePayOrder) {
        //修改用户优惠券记录状态变更成占用中
        if (!CollectionUtils.isEmpty(userCouponIdList)) {
            payBaseService.sendMsgUpdateUserCouponStatus(userCouponIdList, chargePayOrder);
        }
        //修改充电记录状态为支付中
        if (chargePayReqDto.getChargeRecordId() != null) {
            payBaseService.sendChargingOrderStatus(chargePayOrder.getChargeRecordId(), ChargingStatusEnum.PAYING.getStatus());
        }
    }

    private void cacheCouponUsing(List<Long> userCouponIdList, ChargeUserLoginResDto user) {
        userCouponIdList.forEach(userCouponId -> {
            RBucket<Long> bucket = redissonClient.getBucket(ChargeMarketRedisKeyPrefix.USER_USING_COUPON + userCouponId);
            bucket.set(userCouponId);
            //设置过期时间 7天 如果遇到问题，7天有足够的时间解决问题
            bucket.expire(60 * 60 * 24 * 7, TimeUnit.SECONDS);
            redissonClient.getAtomicLong(ChargeMarketRedisKeyPrefix.USER_AVAILABLE_COUPON_COUNT_KEY + user.getId()).decrementAndGet();
        });
    }

    private void checkPayChargingRecordStatus(ChargePayReqDto chargePayReqDto) {
        //是否是支付充电订单
        if (chargePayReqDto.getChargeRecordId() != null) {
            ChargePayOrder chargePayOrder = chargePayOrderMapper.selectByChargeRecordId(chargePayReqDto.getChargeRecordId());
            //如果充电记录已经是支付完成状态，则不允许重复支付
            if (chargePayOrder != null && (chargePayOrder.getPayStatus().equals(PayStatusEnum.PAID.getCode()) ||
                    chargePayOrder.getPayStatus().equals(PayStatusEnum.PAYING.getCode()))) {
                log.error("订单:{} {}重复提交支付单", chargePayReqDto.getChargeRecordId(), PayStatusEnum.getDescByCode(chargePayOrder.getPayStatus()));
                throw new BusinessException("充电订单已支付，请勿重复提交");
            }
        }
    }

    private void checkResubmit(BigDecimal amount, Long userId) {
        //防止重复提交（判定规则：同一用户相同金额的充值订单，5秒内不能重复提交）
        String key = amount.toString() + userId;
        RBucket<Long> resubmit = redissonClient.getBucket(key);
        boolean check = resubmit.setIfExists(userId, 5, TimeUnit.SECONDS);
        if (check) {
            log.error("重复提交支付单");
            throw new BusinessException("请勿重复提交");
        }
    }

    private void checkParams(ChargePayReqDto chargePayReqDto) {
        //校验参数
        if (chargePayReqDto.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new BusinessException("充值金额必须大于0");
        }
        if (chargePayReqDto.getChannelId() == null) {
            throw new BusinessException("请选择支付方式");
        }
    }

    public ChargePayOrder getChargePayOrder(ChargePayReqDto chargePayReqDto, ChargeUserLoginResDto user) {
        ChargePayOrder chargePayOrder = new ChargePayOrder();
        chargePayOrder.setUserId(user.getId());
        chargePayOrder.setChannel(chargePayReqDto.getChannelId());
        chargePayOrder.setOrderNo(CHARGE_PAY_ORDER_NO_PREFIX + System.currentTimeMillis() + user.getId());
        chargePayOrder.setOrderType(chargePayReqDto.getChargeRecordId() == null ? 0 : 1);
        chargePayOrder.setChargeRecordId(chargePayReqDto.getChargeRecordId() == null ? 0 : chargePayReqDto.getChargeRecordId());

        chargePayOrder.setPayStatus(PayStatusEnum.PAYING.getCode());
        chargePayOrder.setCreateTime(new Date());
        chargePayOrder.setUpdateTime(new Date());
        return chargePayOrder;
    }
}
